package com.perceptivesoftware.licensing.server.user;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.UUID;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Component;

import com.perceptivesoftware.licensing.client.common.util.ClientConstants;

@Component
public class UserSessionTracker {
	private static final int RANDOM_UUID_SIZE = 50;
	private static long SESSION_PURGE_MINUTES = ClientConstants.DEFAULT_LOGOFF_INTERVAL_MINUTES + 10;
	private final Log logger = LogFactory.getLog(getClass());

	private final HashMap<String, UserSessionDTO> users = new HashMap<String, UserSessionDTO>();
	private final HashMap<String, Long> editUsers = new HashMap<String, Long>();
	private final ArrayList<UserSessionLogoffListener> logoffListeners = new ArrayList<UserSessionLogoffListener>();

	public String login(final String username, final String userCode)
			throws UserAlreadyLoggedInException {
		synchronized (users) {
			if (users.containsKey(username)) {
				if (new Date().getTime()
						- users.get(username).getHeartbeatTimestamp().getTime() > 1 * 60 * 1000) {
					logger.info("User "
							+ username
							+ " ("
							+ userCode
							+ ") already logged in, but stale - allowing new login.");
					try {
						logoff(username);
					} catch (final UserNotLoggedInException e) {
					}
				} else {
					throw new UserAlreadyLoggedInException();
				}
			}

			logger.info("User " + username + " (" + userCode + ") logged in");
			final String sessionID = generateUSessionID();
			logger.debug("New session ID: " + sessionID);
			users.put(username, new UserSessionDTO(userCode, sessionID));
			return sessionID;
		}
	}

	private String generateUSessionID() {
		String s = UUID.randomUUID().toString();
		if (s.length() > RANDOM_UUID_SIZE) {
			s = s.substring(0, RANDOM_UUID_SIZE);
		}
		return s;
	}

	public void logoff(final String username) throws UserNotLoggedInException {
		final UserSessionDTO removed;
		synchronized (users) {
			removed = users.remove(username);
			if (removed == null) {
				throw new UserNotLoggedInException();
			}
		}
		logger.info("User " + username + " (" + removed.getUserCode()
				+ ") logged off");
		synchronized (editUsers) {
			editUsers.remove(username);
		}
		synchronized (logoffListeners) {
			for (final UserSessionLogoffListener listener : logoffListeners) {
				listener.userLoggedOff(username, removed.getUserCode());
			}
		}
	}

	public void registerLogoffListener(final UserSessionLogoffListener listener) {
		synchronized (logoffListeners) {
			logoffListeners.add(listener);
		}
	}

	public String getCurrentUsercodeForUsername(final String username)
			throws UserNotLoggedInException {
		final UserSessionDTO userSessionDTO = users.get(username);
		if (userSessionDTO == null) {
			throw new UserNotLoggedInException();
		}
		return userSessionDTO.getUserCode();
	}

	public void updateHeartbeatTimestamp(String sessionID, final String username)
			throws UserNotLoggedInException, UserStaleSessionException {
		synchronized (users) {
			final UserSessionDTO userSessionDTO = users.get(username);
			if (userSessionDTO == null) {
				throw new UserNotLoggedInException();
			}
			if (userSessionDTO.getSessionID().equals(sessionID)) {
				userSessionDTO.resetHeartbeatTimestamp();
			} else {
				logger.debug("Heartbeat sessionID from client: " + sessionID
						+ ", from server: " + userSessionDTO.getSessionID());
				throw new UserStaleSessionException();
			}
			logger.debug("Heartbeat: User " + username + " ("
					+ userSessionDTO.getUserCode() + ")");
		}
	}

	public void purgeStaleUsers() {
		final ArrayList<String> toLogOff = new ArrayList<String>();
		synchronized (users) {
			for (final String user : users.keySet()) {
				if (new Date().getTime()
						- users.get(user).getHeartbeatTimestamp().getTime() > SESSION_PURGE_MINUTES * 60 * 1000) {
					toLogOff.add(user);
				}
			}
		}
		for (final String username : toLogOff) {
			try {
				final String usercode = getCurrentUsercodeForUsername(username);
				logoff(username);
				logger.warn("Forced logoff for " + username + " (" + usercode
						+ "). - No client contact for " + SESSION_PURGE_MINUTES
						+ " minutes");
			} catch (final UserNotLoggedInException e) {
			}
		}
	}

	public int getLoggedInUserCount() {
		return users.size();
	}
}
